package org.chatty;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;

import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.LineUnavailableException;

/**
 *
 * Consumers of this class need to implement the ThreadedByter.CallBack
 * interface.
 *
 * The ThreadedByter.CallBack object method <b>must</b> get set prior to
 * the initial invocation of ThreadedByter.start.
 *
 * Every call to ThreadedByter.start followed by ThreadedByter.stop is guaranteed
 * to call one of the ThreadedByter.CallBack object's methods.
 *
 * Subclasses should implement the following methods:
 *
 * <UL>
 *     <LI>public abstract void start_process() throws Exception;</LI>
 *     <LI>public abstract int process_chunk( byte buffer[] ) throws Exception;</LI>
 *     <LI>public abstract void stop_process() throws Exception;</LI>
 * </UL>
 *
 * This class does thread management which means it is sort of messy.
 * 
 * @author Brian Hammond
 *
 */
public abstract class ThreadedByter implements Runnable {

	public interface CallBack {
		public void progress( int count, byte audioData[] );
		public void finished( byte audioData[] );
		public void error( Exception exception );
	};

	private CallBack callBack_;

	private boolean working_;
	private boolean canBeToldToStop_;
	private Thread thread_;

	public ThreadedByter() {
	}

	//// implement these:

	public abstract void start_process() throws Exception;
	public abstract int process_chunk( byte buffer[] ) throws Exception;
	public abstract void stop_process() throws Exception;

	////

	private void init() {
		if ( null == this.getThread() ) {
			this.setThread( new Thread( this ) );
			this.getThread().start();
		}
	}

	////

	public synchronized void start() {
		if ( this.getWorking() ) {
			// TODO: do we need to cancel the callback?
			this.setWorking( false );
		} else {
			this.init();
			this.setWorking( true );
		}
	}

	public synchronized void stop() {
		// believe it or not, a call to stop can 
		// actually come in between the test for working
		// and the start of the loop
		// this makes the command to stop wait until
		// the working loop is actually started
		// this guarantees that every call to start
		// followed by stop will *ALWAYS* produce an
		// invocation to one of the callback object's
		// methods.
		while ( !this.getCanBeToldToStop() ) {
			try { 
				Thread.sleep( 50 );
			} catch( Exception e ) {
			}
		}
		this.setWorking( false );
	}

	public void run() {
		// TODO: this is pretty cheesy!
		while( true ) {
			if ( this.getWorking() ) {
				try { 
					ByteArrayOutputStream byteArrayOutputStream = this.process();
					// TODO: check to see if cancelled?
					this.getCallBack().finished( byteArrayOutputStream.toByteArray() );
				} catch ( Exception e ) {
					this.getCallBack().error( e );
				}
				this.setCanBeToldToStop( false );
				this.setWorking( false );
			} else {
				try { 
					Thread.sleep( 10 );
				} catch( Exception e ) {
				}
			}
		}
	}

	////

	public ByteArrayOutputStream process() throws Exception {
		byte buffer[] = new byte[ 10000 ];
		ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();

		this.start_process();
	
		while ( this.getWorking() ) {
			this.setCanBeToldToStop( true );
			int count = this.process_chunk( buffer );
			if ( 0 >= count ) { 
				break;
			} else {
				byteArrayOutputStream.write( buffer, 0, count );
				byteArrayOutputStream.flush();
				this.getCallBack().progress( count, buffer );
			}
		}

		this.stop_process();

		byteArrayOutputStream.flush();
		return byteArrayOutputStream;
	}

	//// boilerplate: 

	public boolean getWorking() {
		return this.working_;
	}

	public void setWorking( boolean working ) {
		this.working_ = working;
	}

	public CallBack getCallBack() {
		return this.callBack_;
	}

	public void setCallBack( CallBack callBack ) {
		this.callBack_ = callBack;
	}

	public Thread getThread() {
		return this.thread_;
	}

	public void setThread( Thread thread ) {
		this.thread_ = thread;
	}
	
	public boolean getCanBeToldToStop() {
		return this.canBeToldToStop_;
	}

	public void setCanBeToldToStop( boolean canBeToldToStop ) {
		this.canBeToldToStop_ = canBeToldToStop;
	}

};
